package social.hub.oauth.oauth4j;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.oauth4j.OAuth4J;
import org.oauth4j.core.OAuth4JConsumer;
import org.oauth4j.core.OAuth4JService;
import org.oauth4j.core.OAuth4JToken;
import org.oauth4j.util.OAuth4JException;

import social.hub.oauth.OAuthServiceConfig;
import social.hub.oauth.OAuthServiceProvider;
import social.hub.oauth.OAuthToken;
import social.hub.util.UrlBuilder;
import social.hub.util.Utils;

public class OAuth4JServiceProvider implements OAuthServiceProvider {

	public static String readStream(InputStream inputStream) throws IOException {

		BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
		StringBuilder builder = new StringBuilder();

		String line = null;
		while ((line = bufferedReader.readLine()) != null) {
			builder.append(line);
		}

		inputStream.close();

		return builder.toString();

	}

	private final OAuth4JService oauth4JService;
	private final GenericOAuth4JServiceProvider oauth4JServiceProvider;

	private final OAuthServiceConfig serviceConfig;
	private static final String TOKEN_REGEX = "access_token=(\\S*?)(&(\\S*))?";

	private static final OAuth4JToken EMPTY_OAUTH4J_TOKEN = new OAuth4JToken(null, null);

	public OAuth4JServiceProvider(OAuthServiceConfig serviceConfig) {

		this.serviceConfig = serviceConfig;

		try {

			OAuth4JConsumer oAuth4JConsumer = new OAuth4JConsumer(serviceConfig.getAppId(),
					serviceConfig.getApiKey(), serviceConfig.getApiSecret());

			oauth4JService = OAuth4J.createOAuth4JService(oAuth4JConsumer,
					GenericOAuth4JServiceProvider.class, "");

			oauth4JServiceProvider = (GenericOAuth4JServiceProvider) oauth4JService
					.getOAuth4JServiceProvider();

			oauth4JServiceProvider.setServiceConfig(serviceConfig);

			if (Utils.isNotNullOrBlank(serviceConfig.getScope())) {
				oauth4JService.setScope(serviceConfig.getScope());
			}

		} catch (OAuth4JException e) {
			throw new RuntimeException(e);
		}

	}

	@Override
	public OAuthToken getAccessToken(OAuthToken requestToken, Map<String, String> parans) {

		try {

			final OAuth4JToken accessToken = serviceConfig.isOAuthVersion2() ? getOAuth2AcessToken(parans)
					: getOAuth1AccessToken(requestToken, parans);

			OAuthToken result = null;

			if (!isEmptyToken(accessToken)) {
				result = convertToOAuthToken(accessToken);
			}

			return result;

		} catch (Exception e) {
			throw new RuntimeException("Erro getting access token.", e);
		}

	}

	@Override
	public String getAuthorizationUrl(OAuthToken requestToken) {

		try {

			OAuth4JService service = getOAuth4JService();

			OAuth4JToken oAuth4JToken = EMPTY_OAUTH4J_TOKEN;

			if (!serviceConfig.isOAuthVersion2()) {
				oAuth4JToken = convertToOAuth4jToken(requestToken);
			}

			service.setToken(oAuth4JToken);

			String url = service.getAuthenticationURL(serviceConfig.getCallbackUrl());
			url = url.replace("&oauth_token=null", "");

			UrlBuilder builder = new UrlBuilder(url, false);

			if (requestToken != null) {
				builder.addUrlParan("requestToken", requestToken.getToken());
				builder.addUrlParan("requestSecret", requestToken.getSecret());
			}

			return builder.toString();

		} catch (OAuth4JException e) {
			throw new RuntimeException("Erro getting authentication URL.", e);
		}

	}

	@Override
	public OAuthToken getRequestToken() {

		try {

			OAuthToken result = null;

			if (!serviceConfig.isOAuthVersion2()) {
				OAuth4JService service = getOAuth4JService();
				OAuth4JToken token = service.getRequestToken(serviceConfig.getCallbackUrl());
				result = convertToOAuthToken(token);
			}

			return result;

		} catch (OAuth4JException e) {
			throw new RuntimeException("Erro getting request token.", e);
		}

	}

	@Override
	public OAuthServiceConfig getServiceConfig() {
		return serviceConfig;
	}

	private OAuth4JToken convertToOAuth4jToken(OAuthToken token) {
		return new OAuth4JToken(token.getToken(), token.getSecret());
	}

	private OAuthToken convertToOAuthToken(OAuth4JToken token) {
		return new OAuthToken(token.getToken(), token.getTokenSecret());
	}

	private OAuth4JToken extractToken(String response) {

		if (response == null || response.isEmpty()) {
			throw new RuntimeException(
					"Response body is incorrect. Can't extract a token from an empty string");
		}

		Matcher matcher = Pattern.compile(TOKEN_REGEX).matcher(response);

		if (matcher.matches()) {
			String token = Utils.decode(matcher.group(1));
			return new OAuth4JToken(token, "");
		} else {
			throw new RuntimeException(
					"Response body is incorrect. Can't extract a token from this: '" + response
							+ "'");
		}

	}

	private OAuth4JToken getOAuth1AccessToken(OAuthToken requestToken, Map<String, String> parans)
			throws OAuth4JException {

		OAuth4JService service = getOAuth4JService();

		service.setToken(convertToOAuth4jToken(requestToken));
		service.setVerifier(parans.get("oauth_verifier"));

		return service.fetchAccessToken(serviceConfig.getCallbackUrl());

	}

	private OAuth4JToken getOAuth2AcessToken(Map<String, String> parans) throws Exception {

		String url = oauth4JServiceProvider.getAccessTokenEndpoint().getURL();
		url = url.replace("{code}", parans.get("code"));

		String response = sendRequest(url);

		return extractToken(response);

	}

	private OAuth4JService getOAuth4JService() {
		return oauth4JService;
	}

	private boolean isEmptyToken(OAuth4JToken token) {
		return token == null || token.getToken() == null || token.getToken().isEmpty();
	}

	private String sendRequest(String url) throws Exception {

		String response = null;

		try {

			URLConnection con = new URL(url).openConnection();

			con.connect();

			HttpURLConnection httpCon = (HttpURLConnection) con;

			int code = httpCon.getResponseCode();

			if (code != 200) {
				String header = httpCon.getHeaderField("WWW-Authenticate");
				throw new Exception("Server return error code " + code + ". Response: '" + header
						+ "'");
			}

			response = readStream(con.getInputStream());

		} catch (Exception e) {
			throw new Exception("Error sending de request.", e);
		}

		return response;

	}

}
